// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "zenhttp.h"

#include <zencore/iobuffer.h>
#include <zencore/logbase.h>
#include <zencore/thread.h>
#include <zencore/uid.h>
#include <zenhttp/httpcommon.h>

#include <functional>
#include <optional>
#include <unordered_map>

namespace zen {

class CbPackage;
class CompositeBuffer;

/** HTTP client implementation for Zen use cases

	Currently simple and synchronous, should become lean and asynchronous

	This does not attempt to provide an interface which can be used to interface
	with arbitrary services, it's primarily intended for interfacing with UE
	development ecosystem HTTP services like Zen store or Jupiter

 */

struct HttpClientAccessToken
{
	using Clock		= std::chrono::system_clock;
	using TimePoint = Clock::time_point;

	static constexpr int64_t ExpireMarginInSeconds = 30;

	std::string Value;
	TimePoint	ExpireTime;

	bool IsValid() const
	{
		return Value.empty() == false &&
			   ExpireMarginInSeconds < std::chrono::duration_cast<std::chrono::seconds>(ExpireTime - Clock::now()).count();
	}
};

struct HttpClientSettings
{
	std::string											  LogCategory = "httpclient";
	std::chrono::milliseconds							  ConnectTimeout{3000};
	std::chrono::milliseconds							  Timeout{};
	std::optional<std::function<HttpClientAccessToken()>> AccessTokenProvider;
	bool												  AssumeHttp2 = false;
	bool												  AllowResume = false;
	uint8_t												  RetryCount  = 0;
};

class HttpClient
{
public:
	struct Settings
	{
	};
	HttpClient(std::string_view BaseUri, const HttpClientSettings& Connectionsettings = {});
	~HttpClient();

	struct ErrorContext
	{
		int			ErrorCode;
		std::string ErrorMessage;
	};

	struct KeyValueMap
	{
		KeyValueMap() = default;
		std::unordered_map<std::string, std::string> Entries;

		constexpr inline const std::unordered_map<std::string, std::string>* operator->() const { return &Entries; }
		constexpr inline std::unordered_map<std::string, std::string>*		 operator->() { return &Entries; }
		constexpr inline const std::unordered_map<std::string, std::string>& operator*() const { return Entries; }
		constexpr inline std::unordered_map<std::string, std::string>&		 operator*() { return Entries; }

		template<typename T>
		KeyValueMap(T Begin, T End) : Entries(Begin, End)
		{
		}
		KeyValueMap(std::pair<std::string, std::string>&& Entry) : Entries({{Entry}}) {}
		KeyValueMap(std::pair<std::string_view, std::string_view>&& Entry)
		: Entries({{{std::string(Entry.first), std::string(Entry.second)}}})
		{
		}
		KeyValueMap(std::span<std::pair<std::string_view, std::string_view>>&& List) : Entries(List.begin(), List.end()) {}
		KeyValueMap(std::initializer_list<std::pair<std::string_view, std::string_view>>&& List) : Entries(List.begin(), List.end()) {}
	};

	struct Response
	{
		HttpResponseCode StatusCode = HttpResponseCode::ImATeapot;
		IoBuffer		 ResponsePayload;  // Note: this also includes the content type

		// Contains the reponse headers
		KeyValueMap Header;

		// The number of bytes sent as part of the request
		int64_t UploadedBytes;

		// The number of bytes received as part of the response
		int64_t DownloadedBytes;

		// The elapsed time in seconds for the request to execute
		double ElapsedSeconds;

		// This contains any errors from the HTTP stack. It won't contain information on
		// why the server responded with a non-success HTTP status, that may be gleaned
		// from the response payload itself depending on what the server provides.
		std::optional<ErrorContext> Error;

		// Return the response payload as a CbObject. Note that this does not attempt to
		// validate that the content type or content itself makes sense as a CbObject
		CbObject AsObject() const;

		// Return the response payload as a CbPackage. Note that this does not attempt to
		// validate that the content type or content itself makes sense as a CbPackage
		CbPackage AsPackage() const;

		// Return the response payload as a string. Note that this does not attempt to
		// validate that the content type or content itself makes sense as a string.
		std::string_view AsText() const;

		// Return text representation of the payload. Formats into JSON for structured
		// objects, returns text as-is for text types like Text, JSON, HTML etc
		std::string ToText() const;

		// Returns whether the HTTP status code is considered successful (i.e in the
		// 2xx range)
		bool			IsSuccess() const noexcept;
		inline explicit operator bool() const noexcept { return IsSuccess(); }

		void ThrowError(std::string_view ErrorPrefix = "error");

		std::string ErrorMessage(std::string_view Prefix) const;
	};

	[[nodiscard]] Response Put(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {});
	[[nodiscard]] Response Put(std::string_view Url, const KeyValueMap& Parameters = {});
	[[nodiscard]] Response Get(std::string_view Url, const KeyValueMap& AdditionalHeader = {}, const KeyValueMap& Parameters = {});
	[[nodiscard]] Response Head(std::string_view Url, const KeyValueMap& AdditionalHeader = {});
	[[nodiscard]] Response Delete(std::string_view Url, const KeyValueMap& AdditionalHeader = {});
	[[nodiscard]] Response Post(std::string_view Url, const KeyValueMap& AdditionalHeader = {}, const KeyValueMap& Parameters = {});
	[[nodiscard]] Response Post(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {});
	[[nodiscard]] Response Post(std::string_view   Url,
								const IoBuffer&	   Payload,
								ZenContentType	   ContentType,
								const KeyValueMap& AdditionalHeader = {});
	[[nodiscard]] Response Post(std::string_view Url, CbObject Payload, const KeyValueMap& AdditionalHeader = {});
	[[nodiscard]] Response Post(std::string_view Url, CbPackage Payload, const KeyValueMap& AdditionalHeader = {});
	[[nodiscard]] Response Post(std::string_view	   Url,
								const CompositeBuffer& Payload,
								ZenContentType		   ContentType,
								const KeyValueMap&	   AdditionalHeader = {});
	[[nodiscard]] Response Upload(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {});
	[[nodiscard]] Response Upload(std::string_view		 Url,
								  const CompositeBuffer& Payload,
								  ZenContentType		 ContentType,
								  const KeyValueMap&	 AdditionalHeader = {});

	[[nodiscard]] Response Download(std::string_view			 Url,
									const std::filesystem::path& TempFolderPath,
									const KeyValueMap&			 AdditionalHeader = {});

	[[nodiscard]] Response TransactPackage(std::string_view Url, CbPackage Package, const KeyValueMap& AdditionalHeader = {});

	static std::pair<std::string_view, std::string_view> Accept(ZenContentType ContentType)
	{
		return std::make_pair("Accept", MapContentTypeToString(ContentType));
	}

	LoggerRef		 Logger() { return m_Log; }
	std::string_view GetBaseUri() const { return m_BaseUri; }
	bool			 Authenticate();

private:
	const std::optional<HttpClientAccessToken> GetAccessToken();
	struct Impl;

	LoggerRef				 m_Log;
	std::string				 m_BaseUri;
	std::string				 m_SessionId;
	const HttpClientSettings m_ConnectionSettings;
	RwLock					 m_AccessTokenLock;
	HttpClientAccessToken	 m_CachedAccessToken;
	Ref<Impl>				 m_Impl;
};

void httpclient_forcelink();  // internal

}  // namespace zen
